#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on 2024-03-07

Get all resources involved in the neonsan storage node

@author: newton
"""

import signal
import time
import subprocess
import os
import sys
import logging
from ConfigParser import ConfigParser
from optparse import OptionParser

logger = logging.getLogger('get_stor_resource')
log_file = '/tmp/get_stor_resource.log'
_log = logging.FileHandler(log_file, 'a', encoding='utf-8')
formatter = logging.Formatter('%(asctime)s - %(filename)s - line:%(lineno)d - %(levelname)s - %(message)s')
_log.setFormatter(formatter)
logger.addHandler(_log)
logger.setLevel(logging.INFO)


def _get_opt_parser():
    """ get option parser """

    MSG_USAGE = '''%prog -n "node_id,..." [-f <conf_file>]'''
    opt_parser = OptionParser(MSG_USAGE)

    opt_parser.add_option("-n", "--nodes", action="store", type="string",
                          dest="nodes", help='''the comma separated NODEs of neonsan storage node you want to get. ''',
                          default="")

    opt_parser.add_option("-f", "--config", action="store", type="string", dest="conf_file",
                          help='config file, default is /etc/neonsan/center.conf', default="/etc/neonsan/center.conf")

    return opt_parser


def exec_cmd(cmd, remote_host=None, ssh_port=None, hide_password=None):
    logger.debug("start executing cmd on [%s] : [%s]", remote_host, cmd)
    cmd_log = cmd
    if hide_password:
        cmd_log = cmd.replace(" %s " % hide_password, " **** ")

    remote_host_log = "local"
    if remote_host:
        _port = ssh_port
        if _port is None:
            _port = 4120
        remote_host_log = remote_host
        cmd = "ssh -o 'StrictHostKeyChecking no' -o 'UserKnownHostsFile /dev/null' -o ConnectTimeout=10 " \
              "-o ConnectionAttempts=3 -p %d root@%s \"%s\"" % (_port, remote_host, cmd.replace("$", "\\$"))

    p = None
    stime = time.time()
    try:
        p = subprocess.Popen(cmd, shell=True, executable="/bin/bash", close_fds=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        (output, errput) = p.communicate()
    except:
        # timed out
        etime = time.time()
        logger.warn("cmd [%s] on [%s] timeout. exec_time is [%.3f]s" % (cmd_log, remote_host_log, etime - stime))
        if p:
            if "ip netns exec " in cmd:
                exec_cmd_timeout(p, output_pid_stack=True)
            else:
                exec_cmd_timeout(p)
            try:
                p.kill()
                p.wait()
            except:
                pass
        return None

    etime = time.time()
    ret_code = p.returncode
    output = _decode_str(output.strip()) if output else ""
    errput = _decode_str(errput.strip()) if errput else ""
    exec_time = etime - stime

    try:
        p.wait()
    except:
        pass

    # ssh returns 255 when an error occurred
    # but maybe 255 is returned by the remote script
    if remote_host and ret_code == 255:
        logger.warning("ssh %s failed: ret[%s] out[%s] err[%s]",
                       remote_host, ret_code, output, errput)
    if ret_code != 0:
        logger.error("cmd [%s] on [%s] failed: ret[%d] out[%s] err[%s]. exec_time is [%.3f]s" %
                     (cmd_log, remote_host_log, ret_code, output, errput, exec_time))
    else:
        logger.info("cmd [%s] on [%s] done: ret[%d] out[%s] err[%s]. exec_time is [%.3f]s",
                    cmd_log, remote_host_log, ret_code, output, errput, exec_time)
    return ret_code, output, errput


def exec_cmd_timeout(proc, expect_pids=None, output_pid_stack=False):
    # MUST kill all child processes, otherwise children will become zombies
    killed_pids = []
    kill_all_processes(proc.pid, pid_list=killed_pids,
                       output_pid_stack=output_pid_stack)

    if expect_pids:
        try:
            # double kill, in case the parent pid exits too early
            for pid in expect_pids:
                if pid not in killed_pids:
                    logger.debug("killing missed process [%d]" % pid)
                    kill_all_processes(pid)

            proc.wait(5)
        except Exception, e:
            logger.warn("wait for child processes of [%d] to terminate failed: %s" % (proc.pid, e))
            pass


def kill_all_processes(pid, child_only=False,
                       pid_list=None, output_pid_stack=False):
    """ kill pid and its child processes """

    if pid_list is None:
        pid_list = []

    if pid <= 1:
        return

    logger.debug("killing all processes of [%d]" % pid)
    cmd = "ps -o pid --ppid %d --noheaders" % pid
    p = subprocess.Popen(cmd.split(" "), stdout=subprocess.PIPE)

    # output the stack of the pid
    if output_pid_stack:
        output_cmd = "cat /proc/%d/stack" % pid
        p = subprocess.Popen(output_cmd.split(" "), stdout=subprocess.PIPE)
        (stack_output, _) = p.communicate()
        if stack_output is not None and len(stack_output) > 0:
            logger.info("Stack of pid [%d] is below.\n[%s]" % (pid, stack_output))

    try:
        (output, _) = p.communicate()
        if output is not None and len(output) > 0:
            for pid_str in output.split("\n")[:-1]:
                kill_all_processes(int(pid_str), pid_list=pid_list)
    except Exception, e:
        logger.warn("get child processes of [%d] failed: %s" % (pid, e))
        pass

    pid_list.append(pid)
    if not child_only:
        try:
            os.kill(pid, signal.SIGKILL)
        except:
            pass
        logger.debug("killed process [%d]" % pid)

    try:
        p.wait()
    except:
        pass


def _decode_str(s):
    try:
        return s.decode('utf-8')
    except Exception:
        pass

    try:
        return s.decode('gbk')
    except Exception:
        pass

    return s.decode('utf-8', "replace")


def explode_array(list_str, separator=","):
    """ explode list string into array """
    if list_str is None:
        return None
    result = []
    disk_list = list_str.split(separator)
    for disk in disk_list:
        disk = disk.strip()
        if disk != "":
            result.append(disk)
    return result


def load_conf(conf_file):
    db_info = {}
    config = ConfigParser()
    config.read(conf_file)

    db_info['ip'] = config.get('mysql', 'ip').strip('"').split(',')[0]
    db_info['port'] = config.get('mysql', 'port') if config.has_option('mysql', 'port') else 3306
    db_info['user'] = config.get('mysql', 'user').strip('"')
    db_info['pass'] = config.get('mysql', 'pass').strip('"')
    db_info['db_name'] = config.get('mysql', 'db_name').strip('"')

    return db_info


def format_resource(volume_list):
    disk_list = []
    sanc_list = []
    other_list = []
    for vol in volume_list:
        fmt_vol = vol.replace('.img', '').encode('utf-8')
        if fmt_vol.startswith('vos-'):
            disk_list.append(fmt_vol)
        elif fmt_vol.startswith('vosi-'):
            vm_id = fmt_vol[3:]
            sanc_list.append(vm_id)
        else:
            other_list.append(vol.encode('utf-8'))

    print("volume: %s\n" % disk_list)
    print("instance: %s\n" % sanc_list)
    print("others: %s\n" % other_list)

    logger.info("format volume_list [disk: %s, vm: %s, others: %s] successful." % (disk_list, sanc_list, other_list))


def main(args):
    # parser options
    parser = _get_opt_parser()
    (options, _) = parser.parse_args(args)

    node_list = explode_array(options.nodes)
    if node_list is None or len(node_list) == 0:
        print("Error: You must provides the nodes you want to check!")
        logger.error("You must provides the nodes you want to check!")
        exit(1)

    db_info = load_conf(options.conf_file)
    logger.info("init db [%s]." % db_info)

    sql = "select DISTINCT v.name from store as s,replica as r,volume as v where s.name in ('{}') and " \
          "s.id=r.store_id and r.volume_id=v.id and v.full_name not like '%archive%' and v.full_name not like " \
          "'%async_del%';".format("','".join(node_list))
    cmd = "mysql -h %s -u %s -p%s -P %s %s -s -N -e \"%s\"" % (db_info['ip'], db_info['user'], db_info['pass'],
                                                               db_info['port'], db_info['db_name'], sql)
    ret = exec_cmd(cmd)
    if ret[0] != 0 or not ret[1]:
        print("Error: get volumes failed!")
        logger.error("get volumes failed!")
        exit(1)

    volume_list = ret[1].split()
    logger.info("get volumes of store node [%s] successful." % node_list)

    print("The store node %s has the following resources:\n" % node_list)
    format_resource(volume_list)

    return 0


if __name__ == "__main__":
    main(sys.argv[1:])
